package com.ybbase.framework.config.shiro.filter;

import com.ybbase.framework.common.constant.CommonResultCode;
import com.ybbase.framework.common.constant.ConfigConstant;
import com.ybbase.framework.config.shiro.JWTToken;
import com.ybbase.framework.config.shiro.JwtUtils;
import com.ybbase.framework.model.po.system.User;
import com.ybbase.framework.service.system.UserService;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
import org.apache.shiro.web.util.WebUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMethod;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;


public class JwtAuthFilter extends AuthenticatingFilter {
    private final Logger log = LoggerFactory.getLogger(JwtAuthFilter.class);
    private UserService userService;

    public JwtAuthFilter(UserService userService) {
        this.userService = userService;
        this.setLoginUrl("/");
    }

    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
        //对于OPTION请求做拦截，不做token校验
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            return false;
        }
        return super.preHandle(request, response);
    }

    @Override
    protected void postHandle(ServletRequest request, ServletResponse response) {
        this.fillCorsHeader(WebUtils.toHttp(request), WebUtils.toHttp(response));
        request.setAttribute("jwtShiroFilter.FILTERED", true);
    }

    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        if (this.isLoginRequest(request, response)) {
            return true;
        }
        Boolean afterFiltered = (Boolean) (request.getAttribute("jwtShiroFilter.FILTERED"));
        if (BooleanUtils.isTrue(afterFiltered)) {
            return true;
        }
        boolean allowed = false;
        HttpServletResponse httpResponse = WebUtils.toHttp(response);
        try {
            allowed = executeLogin(request, response);
        } catch (IllegalStateException e) {
            log.error("Not found any token");
            this.outStream(CommonResultCode.JWT_DEFECT.getCode(), httpResponse);
        } catch (Exception e) {
            log.error("Error occurs when login", e);
            this.outStream(CommonResultCode.JWT_ABNORMAL.getCode(), httpResponse);
        }
        return allowed || super.isPermissive(mappedValue);
    }

    @Override
    protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception{
        String jwtToken = getAuthzHeader(servletRequest);
        HttpServletResponse httpResponse = WebUtils.toHttp(servletResponse);
        if (StringUtils.isNotBlank(jwtToken)) {
            if(!JwtUtils.isTokenExpired(jwtToken)){
                return new JWTToken(jwtToken);
            }else{
                this.outStream(CommonResultCode.JWT_OVERDER.getCode(), httpResponse);
            }
        }else{
            this.outStream(CommonResultCode.JWT_DEFECT.getCode(), httpResponse);
        }
        return null;
    }

    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        HttpServletResponse httpResponse = WebUtils.toHttp(servletResponse);
        httpResponse.setCharacterEncoding(ConfigConstant.CHARACTER_ENCODING);
        httpResponse.setContentType(ConfigConstant.CONTENT_TYPE_JSON);
        httpResponse.setStatus(HttpStatus.NON_AUTHORITATIVE_INFORMATION.value());
        fillCorsHeader(WebUtils.toHttp(servletRequest), httpResponse);
        return false;
    }

    @Override
    protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception {
        HttpServletResponse httpResponse = WebUtils.toHttp(response);
        String newToken = null;
        if (token instanceof JWTToken) {
            JWTToken jwtToken = (JWTToken) token;
            User user = (User) subject.getPrincipal();
            boolean shouldRefresh = shouldTokenRefresh(JwtUtils.getIssuedAt(jwtToken.getToken()));
            if (shouldRefresh) {
                newToken = userService.generateJwtToken(user.getId());
            }
        }else {
            this.outStream(CommonResultCode.JWT_ABNORMAL.getCode(), httpResponse);
        }
        if (StringUtils.isNotBlank(newToken)) {
            httpResponse.setHeader(ConfigConstant.HEADER_TOKEN_NAME, newToken);
        }
        return true;
    }

    @Override
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
        log.error("Validate token fail, token:{}, error:{}", token.toString(), e.getMessage());
        return false;
    }

    protected String getAuthzHeader(ServletRequest request) {
        HttpServletRequest httpRequest = WebUtils.toHttp(request);
        String header = httpRequest.getHeader(ConfigConstant.HEADER_TOKEN_NAME);
        return StringUtils.removeStart(header, ConfigConstant.TOKEN_BANNER_NAME);
    }

    protected boolean shouldTokenRefresh(Date issueAt) {
        LocalDateTime issueTime = LocalDateTime.ofInstant(issueAt.toInstant(), ZoneId.systemDefault());
        return LocalDateTime.now().minusSeconds(ConfigConstant.TOKEN_REFRESH_INTERVAL).isAfter(issueTime);
    }

    protected void fillCorsHeader(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) {
        httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
        httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,HEAD,PUT,DELETE");
        httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
    }
    /**
     * 输出流
     * @author cg
     * @date 2019-08-29 09:48
     * @param code 错误代码
     * @return
     * @throws
     * @since
     */
    public void outStream(Integer code,HttpServletResponse httpResponse){
        httpResponse.setHeader("Content-type", ConfigConstant.CONTENT_TYPE_HTML);
        httpResponse.setHeader("Access-Control-Allow-Origin", "*");
        httpResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,HEAD,PUT,DELETE");
        httpResponse.setHeader("Access-Control-Allow-Headers", "*");
        OutputStream outputStream = null;
        try {
            outputStream = httpResponse.getOutputStream();
            byte[] JWT_CODE = ("{\"code\":"+code+"}").getBytes(ConfigConstant.CHARACTER_ENCODING);
            outputStream.write(JWT_CODE);
            outputStream.close();
        }catch (IOException eio){
            eio.printStackTrace();
        }
    }

}
